package org.example.brave.http;

import brave.SpanCustomizer;
import brave.Tags;
import brave.internal.Nullable;
import brave.propagation.TraceContext;

/**
 * Use this to control the response data recorded for an {@link TraceContext#sampledLocal() sampled
 * HTTP client or server span}.
 *
 * <p>Here's an example that adds all HTTP status codes, not just the error ones.
 * <pre>{@code
 * httpTracing = httpTracing.toBuilder()
 *   .clientResponseParser((response, context, span) -> {
 *     HttpResponseParser.DEFAULT.parse(response, context, span);
 *     HttpTags.STATUS_CODE.tag(response, context, span);
 *   }).build();
 * }</pre>
 *
 * <p><em>Note</em>: This type is safe to implement as a lambda, or use as a method reference as it
 * is effectively a {@code FunctionalInterface}. It isn't annotated as such because the project has
 * a minimum Java language level 6.
 *
 * @see HttpRequestParser
 * @since 5.10
 */
// @FunctionalInterface, except Java language level 6. Do not add methods as it will break API!
public interface HttpResponseParser {
    HttpResponseParser DEFAULT = new HttpResponseParser.Default();

    /**
     * Implement to choose what data from the http response are parsed into the span representing it.
     *
     * <p><em>Note:</em> This is called after {@link Span#error(Throwable)}. Conventionally, Zipkin
     * handlers will not overwrite a tag named "error" if you set it here based on the response.
     *
     * @see Default
     */
    void parse(HttpResponse response, TraceContext context, SpanCustomizer span);

    /**
     * The default data policy sets the span name to the HTTP route when available, along with the
     * {@linkplain HttpTags#STATUS_CODE "http.status_code"} and {@linkplain Tags#ERROR "error"} tags.
     *
     * <p><h3>Route-based span name</h3>
     * If routing is supported, and a GET didn't match due to 404, the span name will be "get
     * not_found". If it didn't match due to redirect, the span name will be "get redirected". If
     * routing is not supported, the span name is left alone.
     */
    // This accepts response or exception because sometimes http 500 is an exception and sometimes not
    // If this were not an abstraction, we'd use separate hooks for response and error.
    class Default implements HttpResponseParser {

        /**
         * This tags "http.status_code" when it is not 2xx. If the there is no exception and the status
         * code is neither 2xx nor 3xx, it tags "error". This also overrides the span name based on the
         * {@link HttpResponse#method()} and {@link HttpResponse#route()} where possible (ex "get
         * /users/:userId").
         *
         * <p>If you only want to change how exceptions are parsed, override {@link #error(int,
         * Throwable, SpanCustomizer)} instead.
         */
        @Override public void parse(HttpResponse response, TraceContext context, SpanCustomizer span) {
            int statusCode = 0;
            if (response != null) {
                statusCode = response.statusCode();
                String nameFromRoute = spanNameFromRoute(response, statusCode);
                if (nameFromRoute != null) span.name(nameFromRoute);
                if (statusCode < 200 || statusCode > 299) { // not success code
                    HttpTags.STATUS_CODE.tag(response, context, span);
                }
            }
            error(statusCode, response.error(), span);
        }

        @Nullable
        static String spanNameFromRoute(HttpResponse response, int statusCode) {
            String method = response.method();
            if (method == null) return null; // don't undo a valid name elsewhere
            String route = response.route();
            if (route == null) return null; // don't undo a valid name elsewhere
            if (!"".equals(route)) return method + " " + route;
            return catchAllName(method, statusCode);
        }

        /**
         * Override to change what data from the HTTP error are parsed into the span modeling it. By
         * default, this tags "error" as the the status code, if the error parameter was null and the
         * HTTP status is below 1xx or above 3xx.
         *
         * <p>Note: Either the httpStatus can be zero or the error parameter null, but not both. This
         * does not parse the error, as it is assumed that the {@link ErrorParser has done so prior}.
         *
         * <p>Conventionally associated with the tag key "error"
         */
        protected void error(int httpStatus, @Nullable Throwable error, SpanCustomizer span) {
            if (error != null) return; // the call site used Span.error

            // Instrumentation error should not make span errors. We don't know the difference between a
            // library being unable to get the http status and a bad status (0). We don't classify zero as
            // error in case instrumentation cannot read the status. This prevents tagging every response as
            // error.
            if (httpStatus == 0) return;

            // Unlike success path tagging, we only want to indicate something as error if it is not in a
            // success range. 1xx-3xx are not errors. It is endpoint-specific if client codes like 404 are
            // in fact errors. That's why this is overridable.
            if (httpStatus < 100 || httpStatus > 399) {
                span.tag(Tags.ERROR.key(), HttpTags.statusCodeString(httpStatus));
            }
        }

        /** Helps avoid high cardinality names for redirected or absent resources. */
        @Nullable static String catchAllName(String method, int statusCode) {
            switch (statusCode) {
                // from https://tools.ietf.org/html/rfc7231#section-6.4
                case 301:
                case 302:
                case 303:
                case 305:
                case 306:
                case 307:
                    return method + " redirected";
                case 404:
                    return method + " not_found";
                default:
                    return null;
            }
        }
    }
}
